package com.gfl.trivialrestclient.services.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.dao.SaltSource;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import com.gfl.trivialrestclient.dao.RoleDao;
import com.gfl.trivialrestclient.dao.UserDao;
import com.gfl.trivialrestclient.model.Role;
import com.gfl.trivialrestclient.model.User;
import com.gfl.trivialrestclient.security.UserDetailsAdapter;
import com.gfl.trivialrestclient.services.UserService;
import com.gfl.trivialrestclient.services.UserServiceException;

@Service("userService")
public class UserServiceImpl implements UserDetailsService, UserService {
	public static final String DEFAULT_ROLE_NAME = "ROLE_USER";

	private UserDao userDao;
	private RoleDao roleDao;
	private PasswordEncoder passwordEncoder;
	private SaltSource saltSource;

	@Autowired
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}

	@Autowired
	public void setRoleDao(RoleDao roleDao) {
		this.roleDao = roleDao;
	}

	@Autowired
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
	}

	@Autowired
	public void setSaltSource(SaltSource saltSource) {
		this.saltSource = saltSource;
	}

	@Transactional(readOnly = true)
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		if (username == null) {
			throw new IllegalArgumentException(
					"Username value must be not null.");
		}
		if (username.isEmpty()) {
			throw new UsernameNotFoundException(
					"Username with empty name not exists.");
		}

		// find
		User user = userDao.findByUsername(username);
		if (user == null) {
			throw new UsernameNotFoundException("Not found user with username="
					+ username);
		}

		// transform
		UserDetails userDetails = new UserDetailsAdapter(user);

		return userDetails;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#getUserByUserName
	 * (java.lang.String)
	 */
	@Override
	@Transactional(readOnly = true)
	public User getUserByUserName(String username) {
		if (username == null || username.isEmpty()) {
			return null;
		}

		return userDao.findByUsername(username);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#createUser(com.gfl
	 * .trivialrestclient.model.User)
	 */
	@Override
	@Transactional(readOnly = false)
	public User createUser(User user) {
		Role defaultRole = roleDao.findByRoleName(DEFAULT_ROLE_NAME);
		if (defaultRole == null) {
			defaultRole = new Role(DEFAULT_ROLE_NAME);
		}

		// clear other roles if any
		if (!user.getRoles().isEmpty()) {
			user.getRoles().clear();
		}
		user.addRole(defaultRole);

		// encode user password
		String encodedPassword = passwordEncoder.encodePassword(
				user.getPassword(),
				saltSource.getSalt(new UserDetailsAdapter(user)));
		user.setPassword(encodedPassword);

		userDao.save(user);

		return user;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#isUsernameAvailable
	 * (java.lang.String)
	 */
	@Override
	@Transactional(readOnly = true)
	public boolean isUsernameAvailable(String username) {
		return userDao.findByUsername(username) == null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#addRoleToUser(com
	 * .gfl.trivialrestclient.model.User, com.gfl.trivialrestclient.model.Role)
	 */
	@Override
	@Transactional(readOnly = false)
	public void addRoleToUser(User user, Role role) throws UserServiceException {
		if (user == null) {
			throw new IllegalArgumentException(
					"Wrong input. User must be not null");
		}
		if (role == null) {
			throw new IllegalArgumentException(
					"Wrong input. Role must be not null");
		}

		addRoleToUserByIds(user.getId(), role.getId());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#addRoleToUser(java
	 * .lang.Long, java.lang.Long)
	 */
	@Override
	@Transactional(readOnly = false)
	public void addRoleToUser(Long userId, Long roleId)
			throws UserServiceException {
		addRoleToUserByIds(userId, roleId);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#removeRoleFromUser
	 * (com.gfl.trivialrestclient.model.User,
	 * com.gfl.trivialrestclient.model.Role)
	 */
	@Override
	@Transactional(readOnly = false)
	public void removeRoleFromUser(User user, Role role)
			throws UserServiceException {
		if (user == null) {
			throw new IllegalArgumentException(
					"Wrong input. User must be not null");
		}
		if (role == null) {
			throw new IllegalArgumentException(
					"Wrong input. Role must be not null");
		}

		removeRoleFromUserByIds(user.getId(), role.getId());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#removeRoleFromUser
	 * (java.lang.Long, java.lang.Long)
	 */
	@Override
	@Transactional(readOnly = false)
	public void removeRoleFromUser(Long userId, Long roleId)
			throws UserServiceException {
		removeRoleFromUserByIds(userId, roleId);
	}

	/**
	 * Check assertion that selfinvocation of private method executes in
	 * transaction of calling method
	 * 
	 * @throws UserServiceException
	 *             if private method execute not in transaction
	 */
	private void assertInTransaction() throws UserServiceException {
		if (!TransactionSynchronizationManager.isActualTransactionActive()) {
			throw new UserServiceException(
					"Not transactional self invocation method");
		}
	}

	private void addRoleToUserByIds(Long userId, Long roleId)
			throws UserServiceException {
		assertInTransaction();

		if (userId == null) {
			throw new IllegalArgumentException(
					"Wrong input data. userId must be not null");
		}
		if (roleId == null) {
			throw new IllegalArgumentException(
					"Wrong input data. roleId must be not null");
		}

		User user = userDao.findById(userId);
		if (user == null) {
			throw new UserServiceException("User with id=" + userId
					+ "not exits");
		}
		Role role = roleDao.findById(roleId);
		if (role == null) {
			throw new UserServiceException("Role with id=" + roleId
					+ "not exits");
		}

		user.addRole(role);
		userDao.edit(user);
	}

	private void removeRoleFromUserByIds(Long userId, Long roleId)
			throws UserServiceException {
		assertInTransaction();

		if (userId == null) {
			throw new IllegalArgumentException(
					"Wrong input data. userId must be not null");
		}
		if (roleId == null) {
			throw new IllegalArgumentException(
					"Wrong input data. roleId must be not null");
		}

		User user = userDao.findById(userId);
		if (user == null) {
			throw new UserServiceException("User with id=" + userId
					+ "not exits");
		}
		Role role = roleDao.findById(roleId);
		if (role == null) {
			throw new UserServiceException("Role with id=" + roleId
					+ "not exits");
		}

		user.removeRole(role);
		userDao.edit(user);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#removeUser(com.gfl
	 * .trivialrestclient.model.User)
	 */
	@Override
	@Transactional(readOnly = false)
	public void removeUser(User user) throws UserServiceException {
		if (user == null) {
			throw new IllegalArgumentException(
					"Wrong input data. user must be not null");
		}

		Long userId = user.getId();
		if (userId == null) {
			throw new IllegalArgumentException(
					"Wrong input data. userId must be not null");
		}

		removeUserById(userId);
	}

	private void removeUserById(Long userId) {
		assertInTransaction();

		User user = userDao.findById(userId);
		if (user == null) {
			throw new UserServiceException("User with id=" + userId
					+ "not exits");
		}

		userDao.delete(user);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.gfl.trivialrestclient.services.impl.UserService#getRoleByName(java
	 * .lang.String)
	 */
	@Override
	@Transactional(readOnly = true)
	public Role getRoleByName(String roleName) {
		return roleDao.findByRoleName(roleName);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.gfl.trivialrestclient.services.impl.UserService#getAllRoles()
	 */
	@Override
	@Transactional(readOnly = true)
	public List<Role> getAllRoles() {
		return roleDao.findAll();
	}

	@Override
	@Transactional(readOnly = false)
	public void removeRole(Role role) {
		if (role == null) {
			throw new IllegalArgumentException(
					"Wrong input data. role must be not null");
		}
		Long roleId = role.getId();
		if (roleId == null) {
			throw new IllegalArgumentException(
					"Wrong input data. roleID must be not null");
		}

		removeRoleById(roleId);
	}

	@Override
	@Transactional(readOnly = false)
	public void removeRole(Long roleId) {

		if (roleId == null) {
			throw new IllegalArgumentException(
					"Wrong input data. roleID must be not null");
		}

		removeRoleById(roleId);
	}

	private void removeRoleById(Long roleId) {
		assertInTransaction();

		Role findedRole = roleDao.findById(roleId);
		if (findedRole == null) {
			throw new UserServiceException("Role with id=" + roleId
					+ "not exits");
		}

		roleDao.delete(findedRole);
	}

	@Override
	@Transactional(readOnly = false)
	public Role createRole(String roleName) {
		Role role = roleDao.findByRoleName(roleName);
		if (role == null) {
			role = roleDao.save(new Role(roleName));
		}

		return role;
	}
}
